package edu.bu.cs673.superkids123;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.SplashScreen;
import java.awt.Toolkit;
import java.util.HashMap;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;

/**
 * This abstract class is used to represent the main frame of the application.
 * 
 * @author bhepburn
 */
public abstract class AbstractFrame {
	private Class<? extends AbstractView<? extends JComponent>> initialViewClass;

	protected final JFrame frame;

	protected final Map<Class<? extends AbstractView<? extends JComponent>>, AbstractView<? extends JComponent>> views = new HashMap<Class<? extends AbstractView<? extends JComponent>>, AbstractView<? extends JComponent>>();
	protected final Map<Class<? extends AbstractController>, AbstractController> controllers = new HashMap<Class<? extends AbstractController>, AbstractController>();

	private Class<? extends AbstractView<? extends JComponent>> currentView;

	private SplashScreen splash;

	private Graphics2D graphics;

	/**
	 * This is the constructor for the frame.  It registers all the
	 * controllers and views then runs layout.
	 */
	public AbstractFrame() {
		registerAllControllers();
		registerAllViews();
		this.frame = layout();
	}

	/**
	 * This method must be overridden by the subclass to initialize the views.
	 */
	protected abstract void registerAllViews();

	/**
	 * This method must be overridden by the subclass to initialize the controllers.
	 */
	protected abstract void registerAllControllers();

	/**
	 * @return
	 */
	protected abstract JFrame layout();

	/**
	 * This method shows the splash screen and then the frame with
	 * the initial view.
	 *
	 * @return
	 */
	public JFrame show() {
		// Show the splash screen
		showSplashScreen();

		// Set initial view
		changeView(initialViewClass);

		// Close the splash screen
		closeSplashScreen();

		// Show frame
		frame.pack();
		centerFrame(frame);
		frame.setVisible(true);

		return frame;
	}

	/**
	 * This method closes the splash screen
	 */
	private void closeSplashScreen() {
		if (splash != null) {
			splash.close();
		}
	}

	/**
	 * This method is used to render the text in the splash screen during 
	 * startup.
	 */
	private void showSplashScreen() {
		splash = SplashScreen.getSplashScreen();
		if (splash == null) {
			System.out.println("SplashScreen.getSplashScreen() returned null");
			return;
		}
		graphics = splash.createGraphics();
		if (graphics == null) {
			System.out.println("g is null");
			return;
		} else {
			new Thread(new Runnable() {
				@Override
				public void run() {
					StringBuffer s = new StringBuffer(".");
					while (splash.isVisible()) {
						Font lFont = new Font(Font.SANS_SERIF, Font.BOLD, 14);
						graphics.setFont(lFont);
						graphics.drawString(
								"Please wait, loading" + s.toString(), 270, 300);
						splash.update();
						try {
							Thread.sleep(500);
						} catch (InterruptedException e) {
							// Do nothing it's just the splash
						}

						if (s.length() < 10) {
							s.append(".");
						}
					}
				}
			}).start();
		}
	}

	/**
	 * This method prompts the user to make sure they want to exit and then
	 * stops the application if the user says yes.
	 */
	public void exit() {
		int n = JOptionPane.showConfirmDialog(null,
				"Are you sure you want to exit?", "Exit Program",
				JOptionPane.YES_NO_OPTION);

		if (n == JOptionPane.YES_OPTION) {
			frame.dispose();
			System.exit(0);
		}
	}

	/**
	 * This method changes the current view being shown in the frame by class.
	 * 
	 * @param viewClass The class of the view that should be shown
	 * @return True the view was changed
	 */
	public <V extends AbstractView<? extends JComponent>> boolean changeView(
			Class<V> viewClass) {
		AbstractView<? extends JComponent> view = getView(viewClass);
		if (view != null) {
			// Allows current view to perform cleanup if one exists
			if (currentView == null || getView(currentView).cleanupView()) {
				// Allows view to be changed on fly or cancelled
				view = view.initializeView();
				if (view != null) {
					JComponent contentPane = view.getContentPane();
					if (contentPane != null) {
						// View is changing
						currentView = viewClass;

						frame.getContentPane().removeAll();
						frame.getContentPane().add(contentPane);
						frame.validate();
						frame.repaint();
						return true;
					}
				}
			}
		}
		// If we didn't update return false
		return false;
	}

	/**
	 * This method returns the view instance by class.
	 * 
	 * @param viewClass The class for the view that should be returned 
	 * @return The instance of the requested view or null
	 */
	@SuppressWarnings("unchecked")
	public <V extends AbstractView<? extends JComponent>> V getView(
			Class<V> viewClass) {
		return (V) views.get(viewClass);
	}

	/**
	 * This method returns the controller instance by class.
	 * 
	 * @param controllerClass
	 * @return The instance of the requested view of null
	 */
	@SuppressWarnings("unchecked")
	public <C extends AbstractController> C getController(
			Class<C> controllerClass) {
		return (C) controllers.get(controllerClass);
	}

	/**
	 * This class creates new frame with given parameters.
	 * 
	 * @param title The title for the new frame
	 * @param initialView The initial view that should be shown by the frame
	 * @return The new JFrame
	 */
	public <V extends AbstractView<? extends JComponent>> JFrame frame(
			String title,
			Class<? extends AbstractView<? extends JComponent>> initialView) {
		initialViewClass = initialView;

		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		if (title != null)
			frame.setTitle(title);
		frame.setPreferredSize(new Dimension(800, 600));
		frame.setResizable(false);
		return frame;
	}

	/**
	 * This sets the size of the frame
	 * 
	 * @param frame The frame to be resized
	 * @return the resized and centered frame
	 */
	private static JFrame centerFrame(JFrame frame) {
		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
		Dimension frameSize = frame.getSize();
		if (frameSize.height > screenSize.height)
			frameSize.height = screenSize.height;
		if (frameSize.width > screenSize.width)
			frameSize.width = screenSize.width;
		frame.setLocation((screenSize.width - frameSize.width) / 2,
				(screenSize.height - frameSize.height) / 2);
		return frame;
	}

	/**
	 * This method returns the current view being shown in the main frame.
	 * 
	 * @return the current view that is being shown in the frame
	 */
	public Class<? extends AbstractView<? extends JComponent>> getCurrentView() {
		return currentView;
	}
}